package glossos.tree.snapshot;

import glossos.search.LRUCache;
import glossos.tree.Node;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import org.jmock.lib.legacy.ClassImposteriser;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterators;
import com.google.gson.Gson;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;

public class MongoNodeRepository implements NodeRepository{
    private static class MongoConverter{
        private static final Gson gson = new Gson();
        private final NodeRepository repository;
        private final LRUCache<String, Node> jsonCache = new LRUCache<String, Node>(100);

        private MongoConverter(NodeRepository nodeRepository) {
            repository = nodeRepository;
        }

        public BasicDBObject fromNode(Node node){
            BasicDBObject object = new BasicDBObject();
            object.put("id", node.getId());
            object.put("word", node.getWord());
            object.put("frequency", node.getFrequency());
            object.put("parentId", node.getParentId());
            return object;
        }

        public Node toNode(String doc){
            if (!jsonCache.contains(doc)){
                jsonCache.put(doc, gson.fromJson(doc, Node.class));
                return jsonCache.get(doc);
            }
            return jsonCache.get(doc);
        }

        public Node toNode(DBObject object){
            return searchNode(object);
        }

        private Node searchNode(DBObject object) {
            Node result = toNode(object.toString());
            result.setChildren(new PersistentNodeMap(result, repository));
            result.setParent(createNodeProxy(result.getParentId()));
            return result;
        }

        private Node createNodeProxy(Long parentId) {
            return parentId != null && parentId != 0
                ? ClassImposteriser.INSTANCE.imposterise(new NodeProxy(repository, parentId), Node.class)
                : null;
        }
    }

    private final DB database;
    private final DBCollection collection;
    private final MongoConverter converter = new MongoConverter(this);
    private final AtomicLong nextId;

    public MongoNodeRepository(DB database) {
        this.database = database;
        collection = database.getCollection("nodes");
        collection.ensureIndex(new BasicDBObject("id", 1));

        BasicDBObject compositeIndex = new BasicDBObject();
        compositeIndex.put("parentId", 2);
        compositeIndex.put("word", 3);
        collection.ensureIndex(compositeIndex);

        collection.ensureIndex(new BasicDBObject("parentId", 4));
		nextId = new AtomicLong(count());
    }

    public void save(Node node) {
        BasicDBObject doc = converter.fromNode(node);
        if (nodeHasAlreadyBeenInserted(node)){
            collection.update(resultOf(ImmutableMap.of("id", node.getId())).next(), doc);
        } else {
            collection.insert(doc);
        }
    }

    public Node findById(Long id) {
        DBCursor cursor = resultOf(ImmutableMap.of("id", id));
        return extractNodeFrom(cursor);
    }

    public Set<Node> findByParentId(Long parentId) {
        return extractSetFrom(resultOf(ImmutableMap.of("parentId", parentId)));
    }

    public Node findByParentIdAndWord(Long parentId, String word) {
        return extractNodeFrom(resultOf(ImmutableMap.of("parentId", parentId, "word", word)));
    }

    private DBCursor resultOf(Map<String, ? extends Object> map) {
        return collection.find(new BasicDBObject(map));
    }

    private Set<Node> extractSetFrom(DBCursor cursor) {
        Iterator<Node> nodeIterator = Iterators.transform(cursor, new Function<DBObject, Node>(){
            public Node apply(DBObject object) {
                return converter.toNode(object);
            }
        });
        return ImmutableSet.copyOf(nodeIterator);
    }

    private Node extractNodeFrom(DBCursor cursor){
        return cursor.hasNext()
            ? converter.toNode(cursor.next())
            : null;    
    }

    public Long nextId() {
        return nextId.incrementAndGet();
    }

    public Long count() {
        return collection.getCount();
    }

    public void removeAll() {
        database.eval("db.nodes.remove()");
    }

    private boolean nodeHasAlreadyBeenInserted(Node node){
        return node.getFrequency() > 1;
    }
}
